/*
 * dhcpcl.c
 *
 *  Created on: 15-jul-2009
 *      Author: Pablo Izquierdo & Adrián Romero
 *
 *      Contiene el main de la aplicación y recibe los parametros de ejecucion del programa.
 */

#include "constantes.h"
#include "funciones.c"

/*
 * Punto de entrada a la aplicacion.
 */
int main (int argc, const char* argv[]){
	signal(SIGINT, manejadorSIGINT);
	inicializarConstantes();
	VALOR_EXIT = comprobarParametros(argc, argv);

	if (VALOR_EXIT == 0) {
		imprimirTraza(0, PID, NULL);

		if (VALOR_EXIT == 0) {
			inicializar();
			imprimirDebug("main", "Inicializado");
			ejecutar();
			finalizarTodo();
		}
	}
	return VALOR_EXIT;
}

int inicializar(){
	int resultado;

	DIRECCION_HW = NULL;
	TAM_DIRECCION_HW = 6;
	obtenerDireccionHW();
	resultado = inicializarSocketNivelEnlace();
	inicializarSocketArp();
	return resultado;
}

/*
 * Funcion que comprueba el numero de parametros de entrada, el formato de los mismos
 * y asigna los valores a las variables globales.
 */
int comprobarParametros(int argc, const char* argv[]) {
	int resultado = EXIT_NORMAL;
//	int i, estadoInterfaz, c;
//	char *parametro, *errPtr;
	int estadoInterfaz, c;
	char *errPtr;
	char **argumentos = (char **)argv;



	//Se comprueba el numero de parametros
//	if (argc >= 2 && argc <= 11) {

		//Se asigna el parametro de entrada de interfaz

		PARAMETRO_IFACE = (char*) argv[1];
		if(levantarInterfaz(PARAMETRO_IFACE) < 0){
			estadoInterfaz = FALSE;//no se puede levantar porque ya se esta usando
			VALOR_EXIT = EXIT_ERROR;
			resultado = EXIT_ERROR;
		}else{
			estadoInterfaz = TRUE;
//			for (i = 2; i < argc && resultado != -1; i += 2) {



			while ((c = getopt (argc, argumentos, "t:h:a:l:d")) != -1) {
				switch (c) {
				case 't':
//					timeout = atoi(optarg);
					PARAMETRO_TIMEOUT = strtol(optarg, &errPtr, 0);
					//Se comprueba que no haya habido un error de formato
					if (strlen(errPtr) != 0) {
						imprimirErrorParametros(1);
						resultado = EXIT_ERROR;
					}
					imprimirDebug("comprobarParametros", "-t: %d\n", PARAMETRO_TIMEOUT);
					break;
				case 'h':
//					hostname = optarg;
					PARAMETRO_HOSTNAME = optarg;
					imprimirDebug("comprobarParametros", "-h: %s\n", PARAMETRO_HOSTNAME);
					break;
				case 'a':
//					requestedIP = optarg;
					PARAMETRO_ADDRESS = malloc(sizeof(struct in_addr));
					if(inet_aton(optarg, PARAMETRO_ADDRESS) == 0){
						imprimirErrorParametros(3);
						resultado = EXIT_ERROR;
					}
					imprimirDebug("comprobarParametros", "-a: %s\n", optarg);
					break;
				case 'l':
//					leasetime = atoi(optarg);
					PARAMETRO_LEASE = strtol(optarg, &errPtr, 0);
					//Se comprueba que no haya habido un error de formato
					if (strlen(errPtr) != 0) {
						imprimirErrorParametros(2);
						resultado = EXIT_ERROR;
					}
					imprimirDebug("comprobarParametros", "-l: %d\n", PARAMETRO_LEASE);
					break;
				case 'd':
					PARAMETRO_DEBUG = DEBUG_ON;
					imprimirDebug("comprobarParametros", "Debug mode <ON>");
					break;
				case '?':
//					if ((optopt == 't') || (optopt == 'h') || (optopt == 'a') || (optopt == 'l'))
//						fprintf (stderr, "Opcion -%c requiere un argumento.\n", optopt);
//					else if (isprint (optopt))
//						fprintf (stderr, "Unknown option `-%c'.\n", optopt);
//					else
//						fprintf (stderr,
//								"Unknown option character `\\x%x'.\n",
//								optopt);
//					return 1;
					imprimirErrorParametros(0);
					resultado = EXIT_ERROR;
					break;
				default:
//					abort ();
					imprimirErrorParametros(0);
					resultado = EXIT_ERROR;
				}
			}



//			for (i = 2; i < argc && resultado != -1; i += 2) {
//				// Se comprueba que la cadena sea de un parametro acordado
//				parametro = (char*) argv[i];
//				if (strcmp(parametro, "-t") == 0 && i + 1 < argc) {
//					//Se asigna el parametro timeout
//					PARAMETRO_TIMEOUT = strtol(argv[i + 1], &errPtr, 0);
//					//Se comprueba que no haya habido un error de formato
//					if (strlen(errPtr) != 0) {
//						imprimirErrorParametros(1);
//						resultado = EXIT_ERROR;
//					}
//					imprimirDebug("comprobarParametros", "-t: %d\n", PARAMETRO_TIMEOUT);
//				}
//				else if (strcmp(parametro, "-h") == 0 && i + 1 < argc) {
//					parametro = (char*) argv[i + 1];
//					PARAMETRO_HOSTNAME = parametro;
//					imprimirDebug("comprobarParametros", "-h: %s\n", PARAMETRO_HOSTNAME);
//				}
//				else if (strcmp(parametro, "-a") == 0 && i + 1 < argc) {
//					parametro = (char*) argv[i + 1];
//					PARAMETRO_ADDRESS = malloc(sizeof(struct in_addr));
//					if(inet_aton(parametro, PARAMETRO_ADDRESS) == 0){
//						imprimirErrorParametros(3);
//						resultado = EXIT_ERROR;
//					}
//					imprimirDebug("comprobarParametros", "-a: %s\n", parametro);
//				}
//				else if (strcmp(parametro, "-l") == 0 && i + 1 < argc) {
//					//Se asigna el parametro de lease
//					if(strcmp(argv[i+1], "inf") != 0){
//						PARAMETRO_LEASE = strtol(argv[i + 1], &errPtr, 0);
//						//Se comprueba que no haya habido un error de formato
//						if (strlen(errPtr) != 0) {
//							imprimirErrorParametros(2);
//							resultado = EXIT_ERROR;
//						}
//					}
//					imprimirDebug("comprobarParametros", "-l: %d\n", PARAMETRO_LEASE);
//				}
//				else if (strcmp(parametro, "-d") == 0) {
//					//Se activa el modo debug
//					PARAMETRO_DEBUG = DEBUG_ON;
//					imprimirDebug("comprobarParametros", "Debug mode <ON>");
//				}
//				else {
//					//El parametro recibido no es un parametro acordado
//					imprimirErrorParametros(0);
//					resultado = EXIT_ERROR;
//				}
//			}
		}
//	}
//	else {
//		imprimirErrorParametros(0);
//		resultado = EXIT_ERROR;
//	}
	return resultado;
}

void ejecutar(){
	int esRequesting, esAck, tiempoRestante;
	// Se espera un numero aleatorio de segundos entre 1 y 10
	if(PARAMETRO_DEBUG == DEBUG_OFF){
		srandom(time(NULL));
		esperar((random() % 9000) + 1000);
	}

	esAck = FALSE;
	esRequesting = FALSE;
	tiempoRestante = PARAMETRO_TIMEOUT - TEMPORIZADOR_ACTUAL;
	while ((esAck == FALSE) && (VALOR_EXIT == EXIT_NORMAL)) {
		esRequesting = FALSE;
		inicializarTemporizadores();
		tiempoRestante = PARAMETRO_TIMEOUT - TEMPORIZADOR_ACTUAL;
		imprimirDebug("ejecutar","tiempo restante REQUESTING %d", tiempoRestante);
		while (!esRequesting && tiempoRestante >= 0) {
			imprimirDebug("ejecutar","transitarInit");
			if (transitarInit() >= 0){
				imprimirDebug("ejecutar","selecting");
				esRequesting = transitarSelecting();
			}
			imprimirDebug("ejecutar","tiempo restante= %d - %d", PARAMETRO_TIMEOUT, TEMPORIZADOR_ACTUAL);
			tiempoRestante = PARAMETRO_TIMEOUT - TEMPORIZADOR_ACTUAL;
		}
		if (tiempoRestante > 0) {
			imprimirDebug("ejecutar","requesting");
			inicializarTemporizadores();
			tiempoRestante = PARAMETRO_TIMEOUT - TEMPORIZADOR_ACTUAL;
			imprimirDebug("ejecutar","tiempo restante ack %d", tiempoRestante);
			esAck= -1;
			while ((esAck== -1) && tiempoRestante >= 0) {
				esAck = transitarRequesting();
				tiempoRestante = PARAMETRO_TIMEOUT - TEMPORIZADOR_ACTUAL;
			}
			if(esAck < 0){
				esAck = TRUE;
				fprintf(stderr,"FAILURE: Waiting for ACK Timeout reached\n");
				VALOR_EXIT = EXIT_NO_RESPUESTA;
				bajarInterfaz();
			}
		}
		else {
			VALOR_EXIT = EXIT_NO_RESPUESTA;
			fprintf(stderr,"FAILURE: Waiting for DHCPOFFER Timeout reached\n");
			bajarInterfaz();
		}
	}
	if (VALOR_EXIT == EXIT_NORMAL) {
		imprimirDebug("ejecutar","transitarBound");
		VALOR_EXIT = transitarBound();
	}
}

//libera toda la memoria para finalizar el programa
void finalizarTodo(){
	imprimirDebug("finalizarTodo", "");
	free(DIRECCION);
	free(DIRECCION_HW);
	free(MASCARA_RED);
	free(LISTA_ROUTERS);
	free(LISTA_DNS);
	free(NOMBRE_DOMINIO);
	cerrarSocketNivelEnlace();
	close(descriptorSocketDatagramasUDP);
	if(PARAMETRO_ADDRESS != NULL){
		free(PARAMETRO_ADDRESS);
	}
	if(NOMBRE_SERVIDOR != NULL){
		free(NOMBRE_SERVIDOR);
	}
}

// Funcion que imprime los mensajes de error de los parametros de entrada.
void imprimirErrorParametros(int err) {
	switch (err) {
	case 0:
		fprintf(stderr,
				"Error en el numero de parametros, el formato de ejecucion es:\n\n dhcpcl interface [-t timeout] [-h hostname] [-a IP address] [-l leasetime] [-d]\n\n");
		fprintf(stderr,
				"  −t timeout:\n\tEspecifica durante cuanto tiempo el cliente intenta conseguir una ip.\n\tEl valor predeterminado son 64 segundos.\n");
		fprintf(stderr,
				"  −h hostname:\n\tIndica la cadena a usar en el campo de opciones host_name del datagrama DHCP.\n");
		fprintf(stderr,
				"  −a address:\n\tIndica la ultima direccion IP conocida, para ser enviada en el DHCPDISCOVER.\n");
		fprintf(stderr,
				"  −l lease:\n\tEspecifica el valor de tiempo de alquiler pretendido al servidor, el servidor puede sobreescribir este valor.\n\tEl valor predeterminado es infinito\n");
		fprintf(stderr,"  −d:\n\tModo de depuracion.\n");
		break;
	case 1:
		fprintf(stderr,"Error en el formato del valor de timeout.\n");
		fprintf(stderr,
				"  −t timeout: utiliza valores enteros que representan segundos.\n");
		break;
	case 2:
		fprintf(stderr,"Error en el formato del valor de lease.\n");
		fprintf(stderr,
				"  −l lease: utiliza valores enteros que representan segundos.\n");
		break;
	case 3:
		fprintf(stderr,"Error en el formato o del valor de IP address.\n");
		fprintf(stderr,
				"  −a IP address: utiliza notacion decimal separada por puntos.\n");
		break;
	default:
		break;
	}
}

//inicializa las constantes con los valores predeterminados
void inicializarConstantes(){
	PARAMETRO_DEBUG = DEBUG_OFF;
	NO_EXIT = TRUE;
	PARAMETRO_LEASE = 0xffffffff;
	MASCARA_RED = NULL;
	LISTA_ROUTERS = NULL;
	LISTA_DNS = NULL;
	NOMBRE_DOMINIO = NULL;
	PARAMETRO_TIMEOUT = 64;
	inicializarTemporizadores();
	PARAMETRO_HOSTNAME = NULL;
	PARAMETRO_ADDRESS = NULL;
	NOMBRE_SERVIDOR = NULL;
}

// Sale del programa de manera "abrupta"
void manejadorSIGINT(int sigint){
	imprimirTraza(0, DHCPSIGINT, NULL);
	finalizarTodo();
	exit(EXIT_NORMAL);
}

// Hace DHCPRELEASE y baja la interfaz
void manejadorSIGUSR2(int sigusr2){
	//Si recibe señal se envia DHCPRELEASE nuevo socket ip
	imprimirTraza(0, DHCPSIGUSR2, NULL);
	enviarRelease();
	bajarInterfaz(PARAMETRO_IFACE);
	finalizarTodo();
	exit(EXIT_NORMAL);
}

// Hace DHCPRELEASE y baja la interfaz
void manejadorSIGTERM(int sigterm){
	//Si recibe señal se envia DHCPRELEASE nuevo socket ip
	imprimirTraza(0, DHCPSIGUSR2, NULL);
//	enviarRelease();
	bajarInterfaz(PARAMETRO_IFACE);
	finalizarTodo();
	exit(EXIT_NORMAL);
}

//transita al estado BOUND
int transitarBound(){
	ESTADO_ACTUAL=BOUND;
	imprimirTraza(XID,IP,NULL);
	signal(SIGUSR2, manejadorSIGUSR2);
	signal(SIGTERM, SIG_DFL);
	imprimirDebug("transitarBound", "");
	//cerrarSocketNivelEnlace();
	//sleep(PARAMETRO_LEASE); // ahora sobra xq empieza toda la movida de los nuevos estados
//	printf("En bound \n");
	// cuando se quite el sleep de antes se pone este comentario
	operandoEnBound();
	transitarRenewing();

	return EXIT_NORMAL; //quitarlo en la parte completa
}

//transita al estado INIT
int transitarInit(){
	ESTADO_ACTUAL=INIT;
	signal(SIGUSR2, SIG_IGN);
	signal(SIGTERM, manejadorSIGTERM);
	imprimirDebug("transitarInit", "");
	return enviarDiscover();
}

//transita al estado REBINDING
int transitarRebinding(){
	ESTADO_ACTUAL=REBINDING;
	signal(SIGUSR2, manejadorSIGUSR2);
	signal(SIGTERM, SIG_DFL);
	imprimirDebug("transitarRebinding", "");
//	printf("transitarRebinding \n");
	int sal=operandoEnRebinding();

	if(sal!=1){
//		printf("volvemos al inicio \n");
		ejecutar();
	}else{
//		printf("volvemos a Bound \n");
		transitarBound();
	}

	return 0;
}

/*
//transita al estado REBOOTING
int transitarRebooting(){
	return 0;
}
*/

//transita al estado RENEWING
int transitarRenewing(){
	int sal=-1;
	ESTADO_ACTUAL=RENEWING;
	signal(SIGUSR2, manejadorSIGUSR2);
	signal(SIGTERM, SIG_DFL);

//	printf("a la espera de ver q pasa en Renewing \n");
// cuando se quite el sleep de antes se pone este comentario
	sal=operandoEnRenewing();
	if(sal==-1){ //no se recibe ni ACK ni un ACK, se excedio el tiempo
//		printf("vamos a Rebinding pero antes enviamos un REQUEST en broadcast \n");
//enviarRequest(TRUE);
		enviarRequestUDPBroadcast();
		transitarRebinding();
	}else if(sal==1){ // se recibe un ACK
//		printf("vamos a bound \n");
		transitarBound();
	}else if(sal==0){ // se recibe un NACK
//		printf("volvemos al inicio \n");
		ejecutar();
	}

	return 0;
}

//transita al estado REQUESTING
int transitarRequesting(){
	ESTADO_ACTUAL=REQUESTING;
	signal(SIGUSR2, SIG_IGN);
	signal(SIGTERM, manejadorSIGTERM);
	int ack_ok;
	imprimirDebug("transitarRequesting", "");

	// Recibe los mensaje Offer
	// ack_ok 1 -> ack, 0 -> nak, -1 error
	ack_ok = obtenerMensajeAck();
	imprimirDebug("transitarRequesting", "ack_ok %d", ack_ok);
	if (ack_ok > 0) {
		//COMPROBAR QUE LA DIRECCION IP NO ESTA EN USO
		if (estaAsignada() == TRUE) {
			//ENVIAR DHCPDECLINE
			enviarDecline();
			//HAY QUE VOLVER A INIT
			ejecutar();
		}
		else {
			//SE PUEDE SEGUIR NORMALMENTE
			// Establece la configuracion de red del dispositivo con ioctl
			establecerIpDispositivo();
			establecerMascaraDispositivo();//
			if(TAM_LISTA_ROUTERS >0){
				establecerRouterDispositivo();//
			}
			else{
				fprintf(stderr,"WARNING: Waiting no gateway entry\n");
			}
//			imprimirTraza(XID,IP,NULL);
		}
	}
	return ack_ok;
}

//transita al estado SELECTING
int transitarSelecting(){
	ESTADO_ACTUAL=SELECTING;
	signal(SIGUSR2, SIG_IGN);
	signal(SIGTERM, manejadorSIGTERM);
	struct DatosDhcp datosDhcp[NUMERO_MAXIMO_DHCPOFFERS];
	int numeroMensajes;
	int resultado = FALSE;
	int i;
	imprimirDebug("transitarSelecting", "");

	// Recibe los mensaje Offer
	numeroMensajes = obtenerOffers(datosDhcp);
	if (numeroMensajes > 0) {
		//Elegimos ip - no se requiere reservar espacio, se reserva dentro
		establecerInfoMensaje(datosDhcp);
		imprimirDebug("transitarSelecting", "termina selección ip");

		// Envia dhcp request
		resultado = enviarRequest();

		for (i = 0; i < numeroMensajes; i++) {
			free(datosDhcp[i].options);
		}
	}
	return resultado;
}
